'''
tag.py

Copyright (c) 2011 Wiley David Lewis

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Metadata tagging functions allowing nodes to be tagged with arbitrary tags and
searched for by tags.

Interface
    tagNodes(nodes, tags)    Tags node(s) with metadata tag(s)
    deleteTag(nodes, tags)   Removes a tag from a node.
    findTaggedNodes(tags)    Finds all nodes that have all specified tags.
    listTags(nodes)          Lists tags on a node or nodes.
    listAllTags()

Functions taking tags or nodes can be passed a list or a single item.  The way
this is implemented is that we check to see if the arg is a list and if it is,
we re-call the function with each element in the list.  First this is done with
the nodes, then with the tags.  The end result is that the function gets called
once for each possible pair of elements.


'''
# TODO: Doesn't support namespaces
# TODO: Needs support for imports with no namespaces.  A merge tags function...
import maya.cmds as mc


_kNodeAttr = 'tags'
_kNodeAttrGroup = 'metadata'
_kTagAttr = 'taggedNodes'
_kTagNodeType = 'network'
_kTagPrefix = 'meta_'

def _createTagNode(nodeName):
    ''' Creates a tag node - unconditionally '''
    tagnode = mc.createNode(_kTagNodeType, name=nodeName)
    mc.addAttr( tagnode, ln=_kTagAttr,  at='message', multi=True, im=False)
    return tagnode
    
def _getTagNodeName(tagname):
    '''
    Returns a tag node name from the tag name.
    
    This should be the only function that depends on relationship between
    the name of the tag and the name of the node.
    '''
    return (_kTagPrefix + tagname)
    
def _getTagNode(tagName):
    '''
    Gets a tag node from the name, creating the tag node if necessary.
    
    If you only want the name and not the node itself, use _getTagNodeName().
    '''
    nodeName = _getTagNodeName(tagName)
    if not mc.objExists(nodeName):
        tagNode = _createTagNode(nodeName)
    else:
        tagNode = nodeName
        assert mc.nodeType(tagNode) == _kTagNodeType
        assert mc.objExists(tagNode + '.' + _kTagAttr)
    return tagNode

def _addTagAttributes(node):
    ''' DEPRECATED: Adds attributes to the node in preparation for being tagged '''
    assert mc.nodeType(node) == core.nodetypes.Transform
    
    if not mc.objExists(node + '.metadata.tags'):
        mc.addAttr(node, ln='metadata', at='compound', numberOfChildren=1)
        mc.addAttr(node, ln='tags', at='message', multi=True, im=False, parent='metadata')
    
def _connectNode(tagnode, node):
    ''' Connects a tag with a node.'''
    assert mc.nodeType(tagnode) == 'network'
    assert mc.nodeType(node) == 'transform'

    conns = mc.listConnections(node + '.message')
    if conns == None or tagnode not in conns:
        mc.connectAttr(node + '.message', tagnode + '.taggedNodes', na=True)

def tagNodes(nodes, tags):
    '''
    Tags node(s) with metadata tag(s)
    
    This function can be called with a list or single item for either argument.
    '''
    # Convert lists to singular items and re-call the function.
    if hasattr(nodes, "__iter__"):
        for node in nodes:
            tagNodes(node, tags)
        return

    # We're here, so nodes is really just a node (not a list).
    # Now tease out tags.
    if hasattr(tags, "__iter__"):
        for tag in tags:
            tagNodes(nodes, tag)
        return

    # We're here, so nodes and tags are both just a single node and single tag
    tagNode = _getTagNode(tags)
    _connectNode(tagNode, nodes)
    
def deleteTag(nodes, tags):
    '''
    Removes a tag from a node.
    '''
    if hasattr(nodes, "__iter__"):
        for node in nodes:
            deleteTag(node, tags)
        return

    # We're here, so nodes is really just a node
    if hasattr(tags, "__iter__"):
        for tag in tags:
            deleteTag(nodes, tag)
        return
        
    # We're here, so nodes and tags are both just a single node and single tag
    if pnode in mc.listConnections(tag + '.taggedNodes'):
        # Odd, but if you don't want to/can't specify the index, na deletes
        # the first matching connection.
        mc.disconnectAttr(ptag + '.taggedNodes', pnode + '.metadata.tags', na=True)
    
def findTaggedNodes(tags):
    '''
    Finds all nodes that have all specified tags.
    
    
    '''
    if hasattr(tags, "__iter__"):
        nodes = []
        for tag in tags:
            taggednodes = findTaggedNodes(tag)
            nodes = nodes + taggednodes
        return list(set(nodes))
    
    taggednodes = []
    
    # We have the get the name and check for the existence first because if
    # we just call _getTagNode(), we'll create tags for everything we check
    # for.
    tagnode = _getTagNodeName(tags)
    if not mc.objExists(tagnode):
        return []
    
    # Could call _getTagNode() here, but this is easier because we know the
    # node exists.
    for node in mc.listConnections(tagnode + '.taggedNodes'):
        taggednodes.append(str(node))
    return taggednodes

def findTaggedNodesNoPM(tags):
    '''
    Finds all nodes that have all specified tags.
    
    
    '''
    taggednodes = []
    
    # We have the get the name and check for the existence first because if
    # we just call _getTagNode(), we'll create tags for everything we check
    # for.
    tagnode = _getTagNodeName(tags)
    #if not objExists(tagnode):
    #    return []
    
    # Could call _getTagNode() here, but this is easier because we know the
    # node exists.
    taggednodes = [node for node in mc.listConnections(tagnode)]
    return taggednodes

def listTags(nodes):
    '''
    Lists tags on a node or nodes.
    
    If a list of nodes are passed in, you get the union of tags on those nodes.
    '''
    if hasattr(nodes, "__iter__"):
        tags = []
        for node in nodes:
            nodetags = listTags(node)
            tags = tags + nodetags
        return list(set(tags))
    
    tags = []
    for tag in mc.listConnections(nodes + '.message'):
        tags.append(str(tag[len(_kTagPrefix):]))
    return tags
    
def listAllTags():
    '''
    List all tags.
    
    Lists all the tags that have been created - even those that aren't currently
    applied to anything.
    '''
    tags = []
    tagnodes = mc.ls(_kTagPrefix + '*', type=_kTagNodeType)
    for node in tagnodes:
        tags.append(str(node)[len(_kTagPrefix):])
    return tags
